作者:Adam Freeman
翻译:陈广
日期:2018-10-13
MVC 提供了一组内置标签助手,用于对 HTML 元素执行通常需要的转换。本章我将描述在 HTML 表单上操作的标签助手,包括form
、input
、label
、select
、option
和textarea
元素。在第25章中,我描述了其他内置标签助手,它们提供了与表单无关的特性。表24-1为表单标签助手简历。
表 24-1:表单标签助手简历
问题 | 回答 |
---|---|
它们是什么? | 表单标签助手用于转换 HTML 表单元素,这样您就不必编写自定义标签助手来解决最常见的问题。 |
它们有何用途? | 表单标签助手确保 HTML 表单元素(包括表单中的元素,如label 和input )是一致生成的。在大多数情况下,标签助手确保直接使用视图模型类设置id 、name 和for 等重要属性,但一些标签助手也可以生成内容,例如使用option 元素填充select 元素。 |
如何使用它们? | 内置标签助手查找以asp- (如asp-for )为前缀的属性。 |
是否有任何缺陷或限制? | 唯一的限制是必须向标签助手提供模型数据以在select 元素中生成option 元素。在《使用 Select 和 Option 元素》一节中,我描述了这个问题,并提供了一个自定义标签助手来解决这个问题。 |
有没有其他选择? | 您可以在视图中编写 HTML 表单,完全不需要使用标签助手属性。您也可以使用我在第23章中描述的技术编写自己的标签助手。 |
表24-2为本章摘要。
表 24-2:本章摘要
问题 | 解决方案 | 清单 |
---|---|---|
在表单元素上设置action 属性 |
使用表单元素标签助手 | 5 |
防止跨站请求伪造 | 给 action 方法应用ValidateAntiForgeryToken 特性,并在表单元素上可选地将asp-antiforgery 属性设置为true 。 |
6、7 |
在input 元素上设置id 、name 和value 属性 |
应用asp-for 属性 |
8 |
格式化input 元素显示的值 |
将asp-format 属性应用于input 元素或在模型类中应用DisplayFormat 属性。 |
9-12 |
设置label 元素的for 属性和内容 |
应用asp-for 属性 |
13 |
更改已应用asp-for 属性的label 元素的内容 |
将Display 属性应用于模型类属性,并使用Name 属性指定内容 |
14 |
在select 元素上设置id 和name 属性 |
应用asp-for 属性 |
15 |
生成option 元素 |
应用asp-items 属性 |
16-21 |
在textarea 元素上设置id 和name 属性 |
应用asp-for 属性 |
22、23 |
本章我继续使用我在第23章中创建的Cities
项目。对于本章,我希望启用 MVC 附带的内置标签助手,并禁用我在第23章中创建的自定义助手。清单24-1显示了我对视图导入文件所做的更改,在该文件中,我将 Cities 程序集中的助手类的@addTagHelper
表达式替换为一个设置 MVC 标签助手的表达式,该表达式是在一个名为 Microsoft.AspNetCore.Mvc.TagHelpers 的程序集中定义的。
清单 24-1:Views 文件夹下的 _ViewImports.cshtml 文件,更改标签助手
@using Cities.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
清单24-2显示了 Index.cshtml 视图的内容,在该视图中,我删除了自定义标签助手类使用的属性。
清单 24-2:Views/Home 文件夹下的 Index.cshtml 文件的内容
@model IEnumerable<City>
@{ Layout = "_Layout"; }
<table class="table table-sm table-bordered">
<thead class="bg-primary text-white">
<tr>
<th>Name</th>
<th>Country</th>
<th class="text-right">Population</th>
</tr>
</thead>
<tbody>
@foreach (var city in Model)
{
<tr>
<td>@city.Name</td>
<td>@city.Country</td>
<td class="text-right">@city.Population?.ToString("#,###")</td>
</tr>
}
</tbody>
</table>
<a href="/Home/Create" class="btn btn-primary">Create</a>
清单24-3显示了对 Create.cshtml 文件的相应更改,我已经返回到使用标准 HTML 元素,而不使用第23章中使用的属性。
清单 24-3:Views/Home 文件夹下的 Create.cshtml 文件的内容
@model City
@{ Layout = "_Layout"; }
<form method="post" action="/Home/Create">
<div class="form-group">
<label for="Name">Name:</label>
<input class="form-control" name="Name" />
</div>
<div class="form-group">
<label for="Country">Country:</label>
<input class="form-control" name="Country" />
</div>
<div class="form-group">
<label for="Population">Population:</label>
<input class="form-control" name="Population" />
</div>
<button type="submit" class="btn btn-primary">Add</button>
<a class="btn btn-primary" href="/Home/Index">Cancel</a>
</form>
最后是对共享布局的更改,如清单24-4所示。
清单 24-4:Views/Shared 文件夹下的 _Layout.cshtml 文件的内容
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Cities</title>
<link href="/lib/twitter-bootstrap/css/bootstrap.css" rel="stylesheet" />
</head>
<body class="m-1 p-1">
<div>@RenderBody()</div>
</body>
</html>
如果运行应用程序,您将看到城市列表,您可以单击【Create】按钮并填写表单向服务器提交新数据,如图24-1所示。
FormTagHelper
类是form
元素的内置标记助手,用于管理 HTML 表单的配置,以便它们根据应用程序的路由配置确定正确的 action 方法。此签记助手支持表24-3中描述的属性。
表 24-3:表单元素的内置标签助手属性
名称 | 描述 |
---|---|
asp-controller | 此属性用于为action 属性 URL 的路由系统指定controller 值。如果省略,则将使用渲染视图的控制器。 |
asp-action | 此属性用于为action 属性 URL 的路由系统指定action 值的 action 方法。如果省略,则将使用渲染视图的 action。 |
asp-route-* | 名称以asp-route- 开头的属性用于为action 属性 URL 指定附加值,以便asp-route-id 属性用于向路由系统提供id 段的值。 |
asp-route | 此属性用于指定将用于为action 属性生成 URL 的路由的名称。 |
asp-area | 此属性用于指定将用于为action 属性生成 URL 的 area 的名称。 |
asp-antiforgery | 此属性控制是否将防伪信息添加到视图中,如《使用防伪功能》一节所述。 |
FormTagHelper
类的主要目的是使用应用程序的路由配置设置form
元素的action
属性,确保表单数据总是发送至正确的 URL,甚至当路由架构更改时。在清单24-5中,我使用了asp-action
和asp-controller
属性来针对 Home 控制器上的Create
action 方法。
注意:标签助手不设置
method
属性,如果从form
元素中省略它,浏览器将使用 GET 请求将表单数据发送到客户端。正如我在第17章中解释的那样,如果使用表单数据来修改应用程序中的数据,这可能会导致问题。设置method
属性是很好的做法,即使您只是想要 GET 请求,这表示了您还没有忘记选择一个方法类型。
清单 24-5:Views/Home 文件夹下的 Create.cshtml 文件,设置表单目标
@model City
@{ Layout = "_Layout"; }
<form method="post" asp-controller="Home" asp-action="Create">
<div class="form-group">
<label for="Name">Name:</label>
<input class="form-control" name="Name" />
</div>
<div class="form-group">
<label for="Country">Country:</label>
<input class="form-control" name="Country" />
</div>
<div class="form-group">
<label for="Population">Population:</label>
<input class="form-control" name="Population" />
</div>
<button type="submit" class="btn btn-primary">Add</button>
<a class="btn btn-primary" href="/Home/Index">Cancel</a>
</form>
如果运行应用程序,请求 /Home/Create URL,并检查发送给客户端的 HTML,您将看到标签助手向form
元素添加了一个 action 属性,并使用路由系统设置其值,如下所示:
<form method="post" action="/Home/Create">
跨站请求伪造(CSRF)是一种利用 Web 应用程序对用户请求进行身份验证的方法。大多数Web应用程序(包括使用 ASP.NET Core 创建的应用程序)都使用 cookies 来识别哪些请求与特定会话相关,而用户身份通常与特定会话相关联。
CSRF(也称session riding)在http://en.wikipedia.org/wiki/Cross-site_request_forgery中有详细描述,它依赖于用户在使用 Web 应用程序之后访问恶意网站,并且不通过单击注销按钮显式结束他们的会话。应用程序仍然认为用户的会话处于活动状态,浏览器存储的 cookie 还没有过期。恶意站点包含一些 JavaScript 代码,该代码在未经用户同意的情况下向应用程序发送表单请求以执行操作,其中操作的类型将取决于受到攻击的应用程序。由于 JavaScript 代码是由用户的浏览器执行的,所以对应用程序的请求包括会话 cookie,并且应用程序在用户不知情或同意的情况下执行操作。
如果form
元素不包含action
属性 —— 因为它是从具有asp-controller
和asp-acton
属性的路由系统生成的 —— 那么FormTagHelper
类将自动启用反 CSRF 特性,通过将安全令牌添加到表单中隐藏的input
元素,并与 cookie 一起添加到发送到客户端的 HTML 中。只有当请求同时包含 cookie 和表单中的隐藏值(恶意站点无法访问)时,应用程序才会处理该请求。表单的每个请求都会生成一组新的唯一的安全令牌。
如果运行应用程序,请求 /Home/Create URL,并查看发送到浏览器的 HTML,您将看到一个隐藏的input
元素,如下所示:
<input name="__RequestVerificationToken" type="hidden" value="CfDJ8KuVkH8hFlRApe
FBxTrhCFTKZe0B9BKwnWDJqLRUDk__PrEwaeCJmiBbGkwW1ZI816c_TrM5XQkJBeqNI5IL8FhuO
RvjZuYIL-GZvnWZ62OThsZYT02HNX_Lu5LWDNWDdVoS5O5hZtzaoHLeY5lNto" />
如果您使用浏览器的 F12 工具,还可以看到相应的 cookie 添加到响应中。将安全令牌添加到 HTML 响应只是进程的一部分;它们还必须由控制器验证,如清单24-6所示。
清单 24-6:Controllers 文件夹下的 HomeController.cs 文件,验证防伪令牌
using Microsoft.AspNetCore.Mvc;
using Cities.Models;
namespace Cities.Controllers
{
public class HomeController : Controller
{
private IRepository repository;
public HomeController(IRepository repo)
{
repository = repo;
}
public ViewResult Index() => View(repository.Cities);
public ViewResult Create() => View();
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Create(City city)
{
repository.AddCity(city);
return RedirectToAction("Index");
}
}
}
ValidateAntoForgeryToken
属性确保请求包含有效的反 CSRF 令牌,如果它们不存在或不包含预期值,则会抛出异常。
FormTagHelper
类提供asp-antiforgery
属性来覆盖默认的反 CSRF 行为。如果它的属性值设为true
,则即使form
元素具有action
属性,安全令牌也将包含在响应中。如果属性值为false
,那么安全令牌将被禁用。在清单24-7中,我已经显式地启用了该特性,尽管无论如何都会添加安全令牌,因为form
元素上没有定义action
属性。
清单 24-7:Views/Home 文件夹下的 Create.cshtml 文件,启用反 CSRF 特性
@model City
@{ Layout = "_Layout"; }
<form method="post" asp-controller="Home" asp-action="Create"
asp-antiforgery="true">
<div class="form-group">
<label for="Name">Name:</label>
<input class="form-control" name="Name" />
</div>
<div class="form-group">
<label for="Country">Country:</label>
<input class="form-control" name="Country" />
</div>
<div class="form-group">
<label for="Population">Population:</label>
<input class="form-control" name="Population" />
</div>
<button type="submit" class="btn btn-primary">Add</button>
<a class="btn btn-primary" href="/Home/Index">Cancel</a>
</form>
提示:测试反 CSRF 功能需要一些诡计。我通过请求包含表单的 URL(如 /Home/Create),然后使用浏览器的F12开发工具定位并从表单中删除隐藏的
input
元素(或者更改元素的值)。当我填充表单并将其发送到应用程序时,浏览器没有部分所需数据的,请求将失败并显示一个错误页面。
input
元素是 HTML 表单的支柱,它提供了用户可以为应用程序提供非结构化数据的主要手段。InputTagHelper
类用于转换input
元素,以便反映它们收集的视图模型属性的数据类型和格式。使用到的属性在表24-4中描述。
表 24-4:Input 元素的内置标签助手
名称 | 描述 |
---|---|
asp-for | 此属性用于指定input 元素表示的视图模型属性 |
asp-format | 此属性用于为input 元素表示的视图模型属性的值指定格式 |
asp-for
属性用于设置图模型属性的名称,然后会设置input
元素的name
、id
、type
和value
属性。在清单24-8中,我将asp-for
属性应用于 Create.cshtml 视图中的input
元素。
清单 24-8:Views/Home 文件夹下的 Create.cshtml 文件,配置 Input 元素
@model City
@{ Layout = "_Layout"; }
<form method="post" asp-controller="Home" asp-action="Create"
asp-antiforgery="true">
<div class="form-group">
<label for="Name">Name:</label>
<input class="form-control" asp-for="Name" />
</div>
<div class="form-group">
<label for="Country">Country:</label>
<input class="form-control" asp-for="Country" />
</div>
<div class="form-group">
<label for="Population">Population:</label>
<input class="form-control" asp-for="Population" />
</div>
<button type="submit" class="btn btn-primary">Add</button>
<a class="btn btn-primary" href="/Home/Index">Cancel</a>
</form>
如果运行应用程序并请求 /Home/Create URL,您将看到标签助手使用asp-for
属性指定的属性来裁剪每个input
元素,如以下片段(其中省略了反 CSRF 安全令牌):
<form method="post" action="/Home/Create">
<div class="form-group">
<label for="Name">Name:</label>
<input class="form-control" type="text" id="Name" name="Name" value="" />
</div>
<div class="form-group">
<label for="Country">Country:</label>
<input class="form-control" type="text" id="Country"
name="Country" value="" />
</div>
<div class="form-group">
<label for="Population">Population:</label>
<input class="form-control" type="number" id="Population"
name="Population" value="" />
</div>
<button type="submit" class="btn btn-primary">Add</button>
<a class="btn btn-primary" href="/Home/Index">Cancel</a>
</form>
input
元素的type
属性告诉浏览器如何在表单中显示元素。您可以在Population
属性的input
元素中看到此过程的简单结果,此类型属性已被设置为number
。这是因为Population
属性的类型是int?
,标签助手使用type
属性指示浏览器仅接收数字值。
注意:
type
属性的解释方式留给浏览器。并非所有浏览器都响应 HTML5 规范中定义的所有类型值,即使这样做,它们的实现方式也有差异。type
属性可以为您在表单中期望的数据类型提供有用的提示,但您应该使用模型验证功能来确保用户提供可用数据,如第27章所述。
表24-5描述了用于设置input
元素类型属性的不同 C# 属性类型。
表 24-5:C# 属性类型和它们生成的 Input 类型元素
C# 类型 | Input 元素类型属性 |
---|---|
byte, sbyte, int, uint,short, ushort, long,ulong | number |
float, double, decimal | text ,以及用于模型验证的附加属性,如下文所述 |
bool | checkbox |
string | text |
DateTime | datetime |
float
、double
和decimal
类型产生input
元素的类型为text
,因为并非所有浏览器都允许可用于表示该类型的合法值的所有字符。为了向用户提供帮助,标签助手将属性添加到与模型验证功能一起使用的input
元素,我在第27章中对此进行了描述。
您可以通过在input
元素上定义type
属性来覆盖表24-5中显示的默认映射。标签助手不会覆盖您定义的值,它允许您利用可用的不同input
元素类型(如password
或hidden
)或 HTML5 中添加的新类型(如number
)。
这种方法的一个缺点是,您必须记住在为给定模型属性生成input
元素的所有视图中设置type
属性。如果需要覆盖多个视图中的默认映射,可以对 C# 模型类中的属性应用UIHint
特性,指定表24-6中的一个值作为特性参数。
提示:如果模型属性不是表24-5中的类型之一,并且没有使用
UIHint
特性修饰,则标签助手将input
元素的type
属性设置为text
。
表 24-6:UIHint 参数及它们所生成的 Input 类型元素
值 | 输入元素类型属性 |
---|---|
HiddenInput | hidden |
Password | password |
Text | text |
PhoneNumber | tel |
Url | url |
EmailAddress | |
Time | time(此值用于显示DateTime 对象的时间组件) |
Date | date(此值用于显示DateTime 对象的时间组件) |
DateTime-local | datetime-local(此值用于在不提供时区信息的情况下显示DateTime 对象。) |
当 action 方法为视图提供一个视图模型对象时,标签助手使用给定给asp-for
属性的属性值来设置input
元素的值属性。asp-format
属性用于指定如何格式化该数据值。
为了演示,我向 Home 控制器添加了一个新的 action 方法,如清单24-9所示。action 方法从存储库中选择第一个City
对象,并使用它作为创建视图的视图模型。
清单 24-9:Controllers 文件夹下的 HomeController.cs 文件,添加 Action 方法
using Microsoft.AspNetCore.Mvc;
using Cities.Models;
using System.Linq;
namespace Cities.Controllers
{
public class HomeController : Controller
{
private IRepository repository;
public HomeController(IRepository repo)
{
repository = repo;
}
public ViewResult Index() => View(repository.Cities);
public ViewResult Edit() => View("Create", repository.Cities.First());
public ViewResult Create() => View();
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Create(City city)
{
repository.AddCity(city);
return RedirectToAction("Index");
}
}
}
如果运行应用程序,请求 /Home/Edit URL,并检查已发送到浏览器的 HTML,您将看到使用视图模型对象填充了值属性,如下所示:
<input class="form-control" type="number" id="Population"
name="Population" value="8539000" />
asp-format
属性接受将传递给标准 C# 字符串格式系统的值,如清单24-10所示。
清单 24-10:Views/Home 文件夹下的 Create.cshtml 文件,格式化数据值
@model City
@{ Layout = "_Layout"; }
<form method="post" asp-controller="Home" asp-action="Create"
asp-antiforgery="true">
<div class="form-group">
<label for="Name">Name:</label>
<input class="form-control" asp-for="Name" />
</div>
<div class="form-group">
<label for="Country">Country:</label>
<input class="form-control" asp-for="Country" />
</div>
<div class="form-group">
<label for="Population">Population:</label>
<input class="form-control" asp-for="Population" asp-format="{0:#,###}" />
</div>
<button type="submit" class="btn btn-primary">Add</button>
<a class="btn btn-primary" href="/Home/Index">Cancel</a>
</form>
属性值是逐字使用的,这意味着必须包含大括号字符和0:
引用以及所需的格式。如果您运行应用程序并请求 /Home/Edit URL,将看到填充值已被格式化如下:
<input class="form-control" type="number" id="Population"
name="Population" value="8,539,000" />
使用此功能时应谨慎,因为您必须确保应用程序的其余部分配置为支持您使用的格式。在本例中,我通过格式化Population
值创建了一个问题。标签助手已经将input
元素的type
属性设置为number
,使用表24-5中为Population
属性描述的默认映射,但是我指定的格式字符串生成了一个包含非数字字符的value
属性。结果是尊循number
元素类型的浏览器(记住,并非所有浏览器)可能不会在元素中显示任何值。
您还必须确保应用程序能够以您使用的格式解析值。示例应用程序希望接收一个可以解析为int
的Population
值,而包含非数字字符的值将导致验证错误,如第27章所述。
如果您总是想要对一个模型属性使用相同的格式,那么可以使用DisplayFormat
特性装饰 C# 类,此特性定义于System.ComponentModel.DataAnnotations
命名空间。DisplayFormat
特性需要两个参数来设置数据值的格式:DataFormatString
参数指定格式化字符串,ApplyFormatInEditMode
参数指定在编辑值时使用格式化。在清单24-11中,我使用DisplayFormat
特性修饰了Population
属性,使用了应用程序和浏览器都可以作为数字处理的格式。
清单 24-11:Models 文件夹下的 City.cs 文件,将格式化特性应用于模型类
using System.ComponentModel.DataAnnotations;
namespace Cities.Models
{
public class City
{
public string Name { get; set; }
public string Country { get; set; }
[DisplayFormat(DataFormatString = "{0:F2}", ApplyFormatInEditMode = true)]
public int? Population { get; set; }
}
}
asp-format
属性优先于DisplayFormat
特性,因此我从视图中删除了该属性,如清单24-12所示。
清单 24-12:Views/Home 文件夹下的 Create.cshtml 文件,移除格式化属性
@model City
@{ Layout = "_Layout"; }
<form method="post" asp-controller="Home" asp-action="Create"
asp-antiforgery="true">
<div class="form-group">
<label for="Name">Name:</label>
<input class="form-control" asp-for="Name" />
</div>
<div class="form-group">
<label for="Country">Country:</label>
<input class="form-control" asp-for="Country" />
</div>
<div class="form-group">
<label for="Population">Population:</label>
<input class="form-control" asp-for="Population" />
</div>
<button type="submit" class="btn btn-primary">Add</button>
<a class="btn btn-primary" href="/Home/Index">Cancel</a>
</form>
如果运行应用程序并请求 /Home/Edit URL,您将看到Population
值已被格式化为两个部分,如下所示:
<input class="form-control" type="number" id="Population"
name="Population" value="8539000.00" />
label
元素由LabelTagHelper
类进行转换,该类使用视图模型类来确保标签没的无错误和一致性。它只有一个受支持的属性,如表24-7所述。
表 24-7:Label 元素的内置标签助手
名称 | 描述 |
---|---|
asp-for | 此属性用于指定label 元素表示的视图模型属性 |
标签助手将使用视图模型属性的名称来设置for
属性的值和label
元素的内容。在清单24-13中,我已经将asp-for
属性应用于表单中的label
元素,这些元素将由标签助手转换。
清单 24-13:Views/Home 文件夹下的 Create.cshtml 文件,应用 Label 标签助手
@model City
@{ Layout = "_Layout"; }
<form method="post" asp-controller="Home" asp-action="Create"
asp-antiforgery="true">
<div class="form-group">
<label asp-for="Name"></label>
<input class="form-control" asp-for="Name" />
</div>
<div class="form-group">
<label asp-for="Country"></label>
<input class="form-control" asp-for="Country" />
</div>
<div class="form-group">
<label asp-for="Population"></label>
<input class="form-control" asp-for="Population" />
</div>
<button type="submit" class="btn btn-primary">Add</button>
<a class="btn btn-primary" href="/Home/Index">Cancel</a>
</form>
由于label
元素是空的,标签助手将使用模型属性名称作为元素的内容,并设置for
属性,该属性告诉浏览器每个label
与哪个input
元素相关联。如果运行该示例,请求 /Home/Create 或 /Home/Edit URL,并检查发送到浏览器的 HTML,您将看到以下输出元素:
<form method="post" action="/Home/Create">
<div class="form-group">
<label for="Name">Name</label>
<input class="form-control" type="text" id="Name"
name="Name" value="London" />
</div>
<div class="form-group">
<label for="Country">Country</label>
<input class="form-control" type="text" id="Country"
name="Country" value="UK" />
</div>
<div class="form-group">
<label for="Population">Population</label>
<input class="form-control" type="number" id="Population"
name="Population" value="8539000.00" />
</div>
<button type="submit" class="btn btn-primary">Add</button>
<a class="btn btn-primary" href="/Home/Index">Cancel</a>
</form>
通过将Display
属性应用到模型类属性,可以覆盖用作label
元素内容的值,如清单24-14所示。
清单 24-14:Models 文件夹下的 City.cs 文件,更改模型属性描述
using System.ComponentModel.DataAnnotations;
namespace Cities.Models
{
public class City
{
[Display(Name = "City")]
public string Name { get; set; }
public string Country { get; set; }
[DisplayFormat(DataFormatString = "{0:F2}", ApplyFormatInEditMode = true)]
public int? Population { get; set; }
}
}
Name
参数指定要使用的值,而不是属性名称。如果您运行该示例,请求 /Home/Create URL,并检查发送到浏览器的 HTML,将看到label
元素的内容发生了更改,如下所示:
<div class="form-group">
<label for="Name">City</label>
<input class="form-control" type="text" id="Name" name="Name" value="London" />
</div>
注意,for
属性的值没有更改,因此浏览器知道label
元素与特定的input
元素相关联,该input
元素不受Display
属性的影响。
提示:您可以通过自己定义
label
元素来阻止标签助手设置标签元素的内容。如果您希望label
元素包含的不仅仅是属性的名称,这是非常有用的,这就是内置标签助手能够提供的全部内容。
select
和option
元素用于为用户提供一组固定的选项,而不是使用input
元素可以打开的数据条目。input
负责转换select
元素,并支持表24-8中描述的属性。
表 24-8:select 元素的内置标签助手属性
名称 | 描述 |
---|---|
asp-for | 此属性用于指定select 元素表示的视图模型属性 |
asp-items | 此属性用于指定select 元素中包含的选项元素值的源。 |
asp-for
属性设置了for
和id
属性的值,以反映它接收的模型属性。在清单24-15中,我已经将用于Country
属性的input
元素替换为定义了asp-for
属性的select
元素。
清单 24-15:Views/Home 文件夹下的 Create.cshtml 文件,使用 select 元素
@model City
@{ Layout = "_Layout"; }
<form method="post" asp-controller="Home" asp-action="Create"
asp-antiforgery="true">
<div class="form-group">
<label asp-for="Name"></label>
<input class="form-control" asp-for="Name" />
</div>
<div class="form-group">
<label asp-for="Country"></label>
<select class="form-control" asp-for="Country">
<option disabled selected value="">Select a Country</option>
<option>UK</option>
<option>USA</option>
<option>France</option>
<option>China</option>
</select>
</div>
<div class="form-group">
<label asp-for="Population"></label>
<input class="form-control" asp-for="Population" />
</div>
<button type="submit" class="btn btn-primary">Add</button>
<a class="btn btn-primary" href="/Home/Index">Cancel</a>
</form>
我已经用option
元素手动填充了select
元素,这些option
元素为用户提供了一系列国家供用户选择。如果您运行应用程序并请求 /Home/Create URL,将看到发送到浏览器的 HTML 包含以下select
元素:
<select class="form-control" id="Country" name="Country">
<option disabled selected value="">Select a Country</option>
<option>UK</option>
<option>USA</option>
<option>France</option>
<option>China</option>
</select>
如果您请求 /Home/Edit URL 并检查发送到浏览器的 HTML,您将看到视图模型对象的Country
属性的值已用于更改所选的option
元素,如下所示:
<select class="form-control" id="Country" name="Country">
<option disabled selected value="">Select a Country</option>
<option selected="selected">UK</option>
<option>USA</option>
<option>France</option>
<option>China</option>
</select>
选择option
元素的任务由OptionTagHelper
类执行,该类通过TagHelperContext.Items
集合接收来自SelectTagHelper
的指令。正如我在第23章中解释的那样,这个集合由需要协同工作的标签助手使用,当我创建一个自定义标签助手时,我利用SelectTagHelper
在下一节中添加到Items
集合中的数据来解决内置的一个限制。
显式定义select
元素的option
元素是一种有用的方法,在那些始终具有相同的可能值的情况下使用。但如需要提供从数据模型中获取的选项,或在多个视图中需要相同的选项集,且不希望手动维护重复内容的选项的情况下,这种方法就没有用了。
如果您有一组固定的选项要呈现给用户,并且不想在整个应用程序的视图中重复这些选项,那么可以使用枚举。我在 Models 文件夹中添加了一个名为 CountryNames.cs 的类文件,并使用它来定义清单24-16所示的枚举。
清单 24-16:Models 文件夹下的 CountryNames.cs 文件的内容
namespace Cities.Models
{
public enum CountryNames
{
UK,
USA,
France,
China
}
}
您不能在asp-items
属性中直接使用枚举,因为标签助手希望使用一系列SelectListItem
对象。然而,有一个方便的助手方法可以执行所需的转换,如清单24-17所示。
清单 24-17:Views/Home 文件夹下的 Create.cshtml 文件,使用枚举
@model City
@{ Layout = "_Layout"; }
<form method="post" asp-controller="Home" asp-action="Create"
asp-antiforgery="true">
<div class="form-group">
<label asp-for="Name"></label>
<input class="form-control" asp-for="Name" />
</div>
<div class="form-group">
<label asp-for="Country"></label>
<select class="form-control" asp-for="Country"
asp-items="@new SelectList(Enum.GetNames(typeof(CountryNames)))">
<option disabled selected value="">Select a Country</option>
</select>
</div>
<div class="form-group">
<label asp-for="Population"></label>
<input class="form-control" asp-for="Population" />
</div>
<button type="submit" class="btn btn-primary">Add</button>
<a class="btn btn-primary" href="/Home/Index">Cancel</a>
</form>
当使用枚举时,生成option
元素的最佳方法是为asp-items
属性提供一个由枚举值名称填充的SelectList
对象。在幕后,SelectTagHelper
类从IEnumerable<SelectListItem>
生成option
元素,SelectList
类实现了这个接口。
如果运行应用程序并请求 /Home/Create 或 /Home/Edit URL,您将看到发送到浏览器的 HTML 包含一组与枚举中的值相对应的option
元素,如下所示:
<select class="form-control" id="Country" name="Country">
<option disabled selected value="">Select a Country</option>
<option>UK</option>
<option>USA</option>
<option>France</option>
<option>China</option>
</select>
请注意,标签助手只保留了占位符option
元素。您显式定义的任何option
元素都保持不变,这意味着您不必将占位符与数据值混合。
如果需要生成option
元素以反映模型中的数据,那么最简单的方法是通过 view bag 提供生成元素所需的数据,如清单24-18所示。
清单 24-18:Controllers 文件夹下的 HomeController.cs 文件,使用 View Bag 提供数据
using Microsoft.AspNetCore.Mvc;
using Cities.Models;
using System.Linq;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace Cities.Controllers
{
public class HomeController : Controller
{
private IRepository repository;
public HomeController(IRepository repo)
{
repository = repo;
}
public ViewResult Index() => View(repository.Cities);
public ViewResult Edit()
{
ViewBag.Countries = new SelectList(repository.Cities
.Select(c => c.Country).Distinct());
return View("Create", repository.Cities.First());
}
public ViewResult Create()
{
ViewBag.Countries = new SelectList(repository.Cities
.Select(c => c.Country).Distinct());
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Create(City city)
{
repository.AddCity(city);
return RedirectToAction("Index");
}
}
}
Edit
和Create
操作方法将ViewBag.Countries
属性设置为一个SelectList
对象,该对象使用存储库中的City.Country
属性的唯一值填充。在清单24-19中,我使用asp-items
属性告诉标签助手从该 view bag 性中获取option
元素的数据。
清单 24-19:Views/Home 文件夹下的 Create.cshtml 文件,在 option 元素内使用 View Bag
@model City
@{ Layout = "_Layout"; }
<form method="post" asp-controller="Home" asp-action="Create"
asp-antiforgery="true">
<div class="form-group">
<label asp-for="Name"></label>
<input class="form-control" asp-for="Name" />
</div>
<div class="form-group">
<label asp-for="Country"></label>
<select class="form-control" asp-for="Country" asp-items="ViewBag.Countries">
<option disabled selected value="">Select a Country</option>
</select>
</div>
<div class="form-group">
<label asp-for="Population"></label>
<input class="form-control" asp-for="Population" />
</div>
<button type="submit" class="btn btn-primary">Add</button>
<a class="btn btn-primary" href="/Home/Index">Cancel</a>
</form>
如果运行应用程序并请求 /Home/Create 或 /Home/Edit URL,您将看到创建了如下option
元素:
<select class="form-control" id="Country" name="Country">
<option disabled selected value="">Select a Country</option>
<option selected>UK</option>
<option>USA</option>
<option>France</option>
</select>
通过 view bag 传递option
元素所需的数据的问题是,必须记得在渲染使用标签助手的视图的每个 action 方法中生成数据。这会导致代码重复,您可以在清单24-18中了解到这一点,并使正确测试和维护控制器变得更加困难。
一个更好的方法是创建一个自定义标签助手来补充内置的SelectTagHelper
类,我将一个名为 SelectOptionTagHelper.cs 的类文件添加到 Infrastructure/TagHelper 文件夹中,并定义了清单24-20中所示的类。
清单 24-20:Infrastructure/TagHelper 文件夹下的 SelectOptionTagHelper.cs 文件
using System;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Cities.Models;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace Cities.Infrastructure.TagHelpers
{
[HtmlTargetElement("select", Attributes = "model-for")]
public class SelectOptionTagHelper : TagHelper
{
private IRepository repository;
public SelectOptionTagHelper(IRepository repo)
{
repository = repo;
}
public ModelExpression ModelFor { get; set; }
public override async Task ProcessAsync(TagHelperContext context,
TagHelperOutput output)
{
output.Content.AppendHtml(
(await output.GetChildContentAsync(false)).GetContent());
string selected = ModelFor.Model as string;
PropertyInfo property = typeof(City)
.GetTypeInfo().GetDeclaredProperty(ModelFor.Name);
foreach (string country in repository.Cities
.Select(c => property.GetValue(c)).Distinct())
{
if (selected != null && selected.Equals(country,
StringComparison.OrdinalIgnoreCase))
{
output.Content
.AppendHtml($"<option selected>{country}</option>");
}
else
{
output.Content.AppendHtml($"<option>{country}</option>");
}
}
output.Attributes.SetAttribute("Name", ModelFor.Name);
output.Attributes.SetAttribute("Id", ModelFor.Name);
}
}
}
此标签助手使用model-for
属性对select
元素进行操作,并使用依赖注入接收存储库对象,该对象可以独立于渲染视图的控制器访问模型数据。此标签助手定义了异步ProcessAsync
方法,因为它简化了获取和保存select
元素的任何现有内容的过程,该过程是通过GetChildContentAsync
方法完成的。
SelectTagHelper
指示option
元素的名称,这些option
元素应该使用自己的类型作为键,通过Items
集合中的条目进行选择。标签助手获取所选项的列表,并将其与 LINQ 查询的结果结合使用,为存储库中的每个唯一值生成option
元素。在清单24-21中,我更新了select
元素,以便将asp-items
属性替换为model-for
属性,并添加了一个@addTagHelper
表达式,该表达式仅为该视图启用自定义标签助手。
清单 24-21:Views/Home 文件夹下的 Create.cshtml 文件,启用自定义标签助手
@model City
@addTagHelper Cities.Infrastructure.TagHelpers.SelectOptionTagHelper, Cities
@{ Layout = "_Layout"; }
<form method="post" asp-controller="Home" asp-action="Create"
asp-antiforgery="true">
<div class="form-group">
<label asp-for="Name"></label>
<input class="form-control" asp-for="Name" />
</div>
<div class="form-group">
<label asp-for="Country"></label>
<select class="form-control" model-for="Country">
<option disabled selected value="">Select a Country</option>
</select>
</div>
<div class="form-group">
<label asp-for="Population"></label>
<input class="form-control" asp-for="Population" />
</div>
<button type="submit" class="btn btn-primary">Add</button>
<a class="btn btn-primary" href="/Home/Index">Cancel</a>
</form>
新的标签助手生成相同的输出,但不需要内置助手所需的 view bag 数据。我喜欢这种方法,因为它使 action 方法集中于它们的特定任务,并保持应用程序的总体形状。
textarea
元素用于从用户那里获取大量文本,通常用于非结构化数据,如注释或言论。TextAreaTagHelper
负责转换textarea
元素,并支持表24-9中描述的单个属性。
表 24-9:TextArea 元素的内置标签助手属性
名称 | 描述 |
---|---|
asp-for | 此属性用于指定表现textarea 元素的视图模型属性 |
TextAreaTagHelper
相对简单,为asp-for
属性提供的值用于在textarea
元素上设置id
和name
属性。为了演示这个标签助手,我向City
模型类添加了一个新属性,如清单24-22所示。
清单 24-22:Models 文件夹下的 City.cs 文件,添加属性
using System.ComponentModel.DataAnnotations;
namespace Cities.Models
{
public class City
{
[Display(Name = "City")]
public string Name { get; set; }
public string Country { get; set; }
[DisplayFormat(DataFormatString = "{0:F2}", ApplyFormatInEditMode = true)]
public int? Population { get; set; }
public string Notes { get; set; }
}
}
在清单24-23中,我使用asp-for
属性将元素与City
类的Notes
属性关联起来,在Create.cshtml
视图中添加了textarea
元素。
清单 24-23:Views/Home 文件夹下的 Create.cshtml 文件,添加 Text Area
@model City
@addTagHelper Cities.Infrastructure.TagHelpers.SelectOptionTagHelper, Cities
@{ Layout = "_Layout"; }
<form method="post" asp-controller="Home" asp-action="Create"
asp-antiforgery="true">
<div class="form-group">
<label asp-for="Name"></label>
<input class="form-control" asp-for="Name" />
</div>
<div class="form-group">
<label asp-for="Country"></label>
<select class="form-control" model-for="Country">
<option disabled selected value="">Select a Country</option>
</select>
</div>
<div class="form-group">
<label asp-for="Population"></label>
<input class="form-control" asp-for="Population" />
</div>
<div class="form-group">
<label asp-for="Notes"></label>
<textarea class="form-control" asp-for="Notes"></textarea>
</div>
<button type="submit" class="btn btn-primary">Add</button>
<a class="btn btn-primary" href="/Home/Index">Cancel</a>
</form>
如果运行应用程序并请求 /Home/Create 或 /Home/Create URL,您将看到发送到浏览器的 HTML 包含如下所示的textarea
元素:
<div class="form-group">
<label for="Notes">Notes</label>
<textarea id="Notes" name="Notes"></textarea>
</div>
TextAreaTagHelper
相对简单,但它提供了与我在本章中描述的其他form
元素标签助手的一致性。
还有两个与 HTML 表单相关的标签助手,我在表24-10中描述了它们,但我会在第27章中更详细地描述他们。当用户提供的数据不符合应用程序的期望时,这些助手用于向用户提供反馈。
表 24-10:验证标签助手类
名称 | 描述 |
---|---|
ValidationMessage | 此标签助手用于提供有关单个表单元素的验证反馈。 |
ValidationSummary | 此标签助手用于提供有关表单中所有元素的验证反馈。 |
本章我描述了用于转换 HTML 表单元素的内置标签助手。这些标签助手确保表单是直接从模型类生成的,这减少了出错的可能性,并为编写 Razor 视图提供了一致的方法。在下一章中,我将描述对一系列 HTML 元素进行操作的其余内置标签助手。
;